/**
 * Copyright (C) 2011 Angelo Zerr <angelo.zerr@gmail.com> and Pascal Leclercq <pascal.leclercq@gmail.com>
 * 
 * All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS  IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package fr.opensagres.xdocreport.document.registry;

import static java.text.MessageFormat.format;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import fr.opensagres.xdocreport.converter.MimeMapping;
import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.core.io.IEntryInfo;
import fr.opensagres.xdocreport.core.io.XDocArchive;
import fr.opensagres.xdocreport.core.logging.LogUtils;
import fr.opensagres.xdocreport.core.registry.AbstractRegistry;
import fr.opensagres.xdocreport.core.utils.StringUtils;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.discovery.IXDocReportFactoryDiscovery;
import fr.opensagres.xdocreport.document.timing.AlarmTimer;
import fr.opensagres.xdocreport.document.timing.AlarmTimerListener;
import fr.opensagres.xdocreport.document.timing.PooledAlarmTimer;
import fr.opensagres.xdocreport.template.ITemplateEngine;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.cache.ITemplateCacheInfoProvider;

/**
 * Registry for loading and cahing {@link IXDocReport} instances.
 */
public class XDocReportRegistry extends AbstractRegistry<IXDocReportFactoryDiscovery> implements
        ITemplateCacheInfoProvider, Serializable
{

    /**
     * 
     */
    private static final long serialVersionUID = 1653550067972102340L;

    /**
     * Logger for this class
     */
    private static final Logger LOGGER = LogUtils.getLogger(AbstractRegistry.class.getName());

    private static final String FILES_TYPE_ERROR = "Impossible to create report for the input stream. The report loader supports only [{0}] files type.";
    private static final XDocReportRegistry INSTANCE = new XDocReportRegistry();

    private final Collection<IXDocReportFactoryDiscovery> reportFactoryDiscoveries = new ArrayList<IXDocReportFactoryDiscovery>();
    private final Map<String, IXDocReport> cachedReports = new HashMap<String, IXDocReport>();

    public XDocReportRegistry() {
        super(IXDocReportFactoryDiscovery.class);
    }

    public static XDocReportRegistry getRegistry() {
        return INSTANCE;
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream) throws IOException, XDocReportException {
        return loadReport(sourceStream, null, null, null, true);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, boolean cacheReport) throws IOException,
            XDocReportException {
        return loadReport(sourceStream, null, null, null, cacheReport);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId) throws IOException, XDocReportException {
        return loadReport(sourceStream, reportId, true);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId, boolean cacheReport) throws IOException,
            XDocReportException {
        return loadReport(sourceStream, reportId, null, null, cacheReport);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId, ITemplateEngine templateEngine)
            throws IOException, XDocReportException {
        return loadReport(sourceStream, reportId, null, templateEngine, true);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId, ITemplateEngine templateEngine,
            boolean cacheReport) throws IOException, XDocReportException {
        return loadReport(sourceStream, reportId, null, templateEngine, cacheReport);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId, String templateEngineKind)
            throws IOException, XDocReportException {
        return loadReport(sourceStream, reportId, templateEngineKind, true);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId, String templateEngineKind,
            boolean cacheReport) throws IOException, XDocReportException {
        return loadReport(sourceStream, reportId, templateEngineKind, null, cacheReport);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId, TemplateEngineKind templateEngineKind)
            throws IOException, XDocReportException {
        return loadReport(sourceStream, reportId, templateEngineKind, true);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param reportId
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, String reportId, TemplateEngineKind templateEngineKind,
            boolean cacheReport) throws IOException, XDocReportException {
        return loadReport(sourceStream, reportId, templateEngineKind.name(), null, cacheReport);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param templateEngine
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, ITemplateEngine templateEngine) throws IOException,
            XDocReportException {
        return loadReport(sourceStream, templateEngine, true);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param templateEngine
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, ITemplateEngine templateEngine, boolean cacheReport)
            throws IOException, XDocReportException {
        return loadReport(sourceStream, null, null, templateEngine, cacheReport);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param templateEngine
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, TemplateEngineKind templateEngineKind) throws IOException,
            XDocReportException {
        return loadReport(sourceStream, templateEngineKind, true);
    }

    /**
     * Load report.
     * 
     * @param sourceStream
     * @param templateEngine
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public IXDocReport loadReport(InputStream sourceStream, TemplateEngineKind templateEngineKind, boolean cacheReport)
            throws IOException, XDocReportException {
        return loadReport(sourceStream, null, templateEngineKind.name(), null, cacheReport);
    }

    private IXDocReport loadReport(InputStream sourceStream, String reportId, String templateEngineKind,
            ITemplateEngine templateEngine, boolean cacheReport) throws IOException, XDocReportException {
        initializeIfNeeded();
        // 2) zip was loaded, create an instance of report
        IXDocReport report = createReport(sourceStream);
        // 3) Update the report id if need.
        if (StringUtils.isEmpty(reportId)) {
            reportId = report.toString();
        }
        report.setId(reportId);
        // 4) Search or set the template engine.
        if (templateEngine == null && StringUtils.isNotEmpty(templateEngineKind)) {
            // Template engine was not forced.
            // Search template engine
            String documentKind = report.getKind();
            templateEngine = TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine(templateEngineKind,
                    documentKind);
            if (templateEngine == null) {
                templateEngine = TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine(templateEngineKind,
                        null);
            }
        }
        report.setTemplateEngine(templateEngine);
        if (cacheReport) {
            registerReport(report);
        }
        return report;
    }

    public IXDocReport createReport(InputStream sourceStream) throws IOException, XDocReportException {
        // Load zipped XML document
        XDocArchive documentArchive = XDocArchive.readZip(sourceStream);
        return createReport(documentArchive);
    }

    public IXDocReport createReport(XDocArchive documentArchive) throws IOException, XDocReportException {
        initializeIfNeeded();
        for (IXDocReportFactoryDiscovery discovery : reportFactoryDiscoveries) {
            if (discovery.isAdaptFor(documentArchive)) {
                IXDocReport report = discovery.createReport();
                if (report != null) {
                    report.setDocumentArchive(documentArchive);
                }
                return report;
            }
        }

        throw new XDocReportException(format(FILES_TYPE_ERROR, getFilesType()));
    }

    private String getFilesType() {
        StringBuilder filesType = new StringBuilder();
        Collection<IXDocReportFactoryDiscovery> discoveries = getReportFactoryDiscoveries();
        for (IXDocReportFactoryDiscovery discovery : discoveries) {
            if (filesType.length() > 0) {
                filesType.append(",");
            }
            filesType.append(discovery.getMimeMapping().getExtension());
        }
        return filesType.toString();
    }

    /**
     * Register report and throws XDocReportException if it already exists a
     * report in the registry with the same id .
     * 
     * @param report
     * @throws XDocReportException
     */
    public void registerReport(IXDocReport report) throws XDocReportException {
        registerReport(report, false);
    }

    /**
     * Register report.
     * 
     * @param report
     *        the report to register
     * @param force
     *        true if report must be forced (if report already exists with
     *        the same id) and false otherwise (throw XDocReportException if
     *        report exists with the same id).
     * @throws XDocReportException
     */
    public synchronized void registerReport(IXDocReport report, boolean force) throws XDocReportException {
        String reportId = report.getId();
        if (StringUtils.isEmpty(reportId)) {
            throw new XDocReportException("Cannot register report. IXDocReport#getId() cannot be empty.");
        }
        if (!force) {
            checkReportId(reportId);
        }
        cachedReports.put(report.getId(), report);
    }

    /**
     * Check if registry can register the report with this id.
     * 
     * @param reportId
     * @throws XDocReportException
     */
    public void checkReportId(String reportId) throws XDocReportException {
        if (cachedReports.containsKey(reportId)) {
            String msg = String.format("Cannot register report. A report with id=%s already exists in the registry",
                    reportId);
            LOGGER.warning(msg);
            throw new XDocReportException(msg);
        }
    }

    /**
     * Returns the report identified with the given id.
     * 
     * @param reportId
     * @return
     */
    public IXDocReport getReport(String reportId) {
        return cachedReports.get(reportId);
    }

    /**
     * Returns true if report identified with the given id exists in the
     * registry and false otherwise.
     * 
     * @param id
     * @return
     */
    public boolean existsReport(String reportId) {
        return cachedReports.containsKey(reportId);
    }

    /**
     * Unregister report identified with the given id.
     * 
     * @param reportId
     */
    public void unregisterReport(String reportId) {
        if (existsReport(reportId)) {
            cachedReports.remove(reportId);
        }
    }

    /**
     * Unregister report.
     * 
     * @param report
     */
    public void unregisterReport(IXDocReport report) {
        if (report != null) {
            cachedReports.remove(report.getId());
        }
    }

    /**
     * Returns list of report factory discoveries.
     * 
     * @return
     */
    public Collection<IXDocReportFactoryDiscovery> getReportFactoryDiscoveries() {
        initializeIfNeeded();

        return reportFactoryDiscoveries;
    }

    // /**
    // * Returns true if template engine is default and false otherwise.
    // *
    // * @param templateEngine
    // * @return
    // */
    // public boolean isDefault(ITemplateEngine templateEngine) {
    // ITemplateEngine defaultTemplateEngine = reportLoader
    // .getDefaultTemplateEngine();
    // if (defaultTemplateEngine == null) {
    // return false;
    // }
    // return defaultTemplateEngine.equals(templateEngine);
    // }

    /**
     * Returns cached report {@link IXDocReport};
     * 
     * @return
     */
    public Collection<IXDocReport> getCachedReports() {
        return Collections.unmodifiableCollection(cachedReports.values());
    }

    public void setClearTimeout(long timeout) {
        PooledAlarmTimer pooledAlarmTimer = new PooledAlarmTimer(timeout);
        pooledAlarmTimer.addAlarmTimerListener(new AlarmTimerListener() {

            public void alarm(AlarmTimer timer) {
                clear();

            }
        });
    }

    /**
     * Clear the cached reports.
     */
    public void clear() {
        cachedReports.clear();
    }

    /**
     * Returns mime mapping for the file extension.
     * 
     * @param fileExtension
     * @return
     * @throws IOException
     * @throws XDocReportException
     */
    public MimeMapping getMimeMapping(String fileExtension) throws IOException, XDocReportException {
        initializeIfNeeded();
        for (IXDocReportFactoryDiscovery discovery : reportFactoryDiscoveries) {
            if (discovery.isAdaptFor(fileExtension)) {
                return discovery.getMimeMapping();
            }
        }
        return null;
    }

    /**
     * Generate unique report id.
     * 
     * @param reportId
     * @return
     */
    public synchronized String generateUniqueReportId(String reportId) {
        IXDocReport report = getReport(reportId);
        if (report != null) {
            StringBuilder id = new StringBuilder(reportId);
            id.append("_");
            id.append(System.currentTimeMillis());
            return id.toString();
        }
        return reportId;
    }

    public IEntryInfo getTemplateCacheInfo(String reportId, String entryName) {
        IXDocReport report = getReport(reportId);
        if (report == null) {
            return null;
        }
        return report.getPreprocessedDocumentArchive().getEntryInfo(entryName);
    }

    @Override
    protected void doDispose() {
        this.reportFactoryDiscoveries.clear();
        this.cachedReports.clear();
    }

    @Override
    protected boolean registerInstance(IXDocReportFactoryDiscovery instance) {
        reportFactoryDiscoveries.add(instance);
        return true;
    }
}
